Purpose: Decode AIS message (payload) into a readable format¶

Output: Separated by fields¶

In [1]:
"""
AIS Message Decoder for Final Year Project
Purpose: Decode different types of AIS messages from NMEA format
Output: TXT format with all the navigation fields
Reference: https://gpsd.gitlab.io/gpsd/AIVDM.html#_json_ais_encoding
"""

import sys

# convert AIS character to 6-bit value
def convert_ais_char(char):
    ascii_value = ord(char)
    val = ascii_value - 48
    if val > 40:
        val = val - 8
    return val

# extract bits from binary string
def extract_bits(binary_data, start_pos, bit_length):
    if start_pos + bit_length > len(binary_data):
        return None
    bit_string = binary_data[start_pos:start_pos + bit_length]
    return int(bit_string, 2)

# extract signed bits (for negative values)
def extract_signed_bits(binary_data, start_pos, bit_length):
    value = extract_bits(binary_data, start_pos, bit_length)
    if value is None:
        return None
    # check if negative (MSB is 1)
    if value >= (1 << (bit_length - 1)):
        value = value - (1 << bit_length)
    return value

# convert payload to binary
def convert_payload_to_binary(payload):
    result = ""
    for char in payload:
        six_bit_val = convert_ais_char(char)
        binary_str = format(six_bit_val, '06b')
        result += binary_str
    return result

# parse NMEA sentence to get payload
def get_payload_from_nmea(sentence):
    if not sentence.startswith('!AIVDM') and not sentence.startswith('!AIVDO'):
        return None
    parts = sentence.strip().split(',')
    if len(parts) >= 6:
        return parts[5]
    return None

# format coordinates with hemisphere
def format_lat_lon(coordinate, is_lon=True):
    if coordinate is None:
        return None, None
    
    if is_lon:
        if coordinate >= 0:
            hemisphere = 'E'
        else:
            hemisphere = 'W'
    else:  # latitude
        if coordinate >= 0:
            hemisphere = 'N'
        else:
            hemisphere = 'S'
    
    return abs(coordinate), hemisphere

# extract text from 6-bit encoded field
def extract_text(binary_data, start_pos, num_chars):
    text = ""
    for i in range(num_chars):
        char_bits = extract_bits(binary_data, start_pos + (i * 6), 6)
        if char_bits is None:
            break
        if char_bits < 32:
            ascii_char = char_bits + 64
        else:
            ascii_char = char_bits
        if ascii_char >= 32 and ascii_char <= 126:
            text += chr(ascii_char)
        else:
            text += ' '
    return text.strip()

# main decode function
def decode_ais(nmea_sentence):
    payload = get_payload_from_nmea(nmea_sentence)
    if not payload:
        return None
        
    binary_data = convert_payload_to_binary(payload)
    if len(binary_data) < 38:
        return None
    
    # get basic message info
    msg_type = extract_bits(binary_data, 0, 6)
    repeat_ind = extract_bits(binary_data, 6, 2)
    mmsi = extract_bits(binary_data, 8, 30)
    
    # initialize all variables
    nav_status = None
    rot = None
    sog = None
    pos_accuracy = None
    longitude = None
    latitude = None
    lat_hem = None
    lon_hem = None
    cog = None
    heading = None
    utc_sec = None
    raim = None
    sync = None
    slot = None
    ship_name = None
    ship_type = None
    destination = None
    draught = None
    imo = None
    callsign = None
    dim_a = None
    dim_b = None
    dim_c = None
    dim_d = None
    ais_version = None
    dte = None
    altitude = None
    aid_type = None
    name_extension = None
    off_position = None
    gnss = None
    
    # decode based on message type
    if msg_type in [1, 2, 3] and len(binary_data) >= 168:
        # Class A position reports
        nav_status = extract_bits(binary_data, 38, 4)
        
        # Rate of turn calculation - GPSD standard
        rot_raw = extract_signed_bits(binary_data, 42, 8)
        if rot_raw == -128:
            rot = "-128.0"  # Not available (default)
        elif rot_raw == -127:
            rot = "-720.0"  # Turning left at more than 5°/30s
        elif rot_raw == 127:
            rot = "+127.0"  # Turning right at more than 5°/30s
        elif rot_raw is not None:
            if rot_raw == 0:
                rot = "+0.0"
            else:
                # GPSD formula: ROT degrees/min = (ROT_raw/4.733)^2 * sign(ROT_raw)
                if rot_raw > 0:
                    rot_degrees = (rot_raw / 4.733) ** 2
                    rot = f"+{rot_degrees:.1f}"
                else:
                    rot_degrees = (abs(rot_raw) / 4.733) ** 2
                    rot = f"-{rot_degrees:.1f}"
        else:
            rot = "+0.0"
        
        # Speed over ground
        sog_raw = extract_bits(binary_data, 50, 10)
        if sog_raw == 1023:
            sog = "0.0"
        else:
            sog = f"{sog_raw / 10.0:.1f}"
        
        pos_accuracy = extract_bits(binary_data, 60, 1)
        
        # Position decoding
        lon_raw = extract_signed_bits(binary_data, 61, 28)
        lat_raw = extract_signed_bits(binary_data, 89, 27)
        
        # check for valid longitude
        if lon_raw != 0x6791AC0:
            lon_degrees = lon_raw / 600000.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
        
        # check for valid latitude  
        if lat_raw != 0x3412140:
            lat_degrees = lat_raw / 600000.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
        
        # Course over ground
        cog_raw = extract_bits(binary_data, 116, 12)
        if cog_raw >= 3600:
            cog = "360.0"
        else:
            cog = f"{cog_raw / 10.0:.1f}"
        
        # True heading
        hdg_raw = extract_bits(binary_data, 128, 9)
        if hdg_raw == 511:
            heading = 511
        else:
            heading = hdg_raw
        
        # UTC second
        utc_raw = extract_bits(binary_data, 137, 6)
        if utc_raw >= 60:
            utc_sec = 60
        else:
            utc_sec = utc_raw
        
        # Communication state
        raim = extract_bits(binary_data, 148, 1)
        sync = extract_bits(binary_data, 149, 2)
        slot = extract_bits(binary_data, 151, 3)
    
    elif msg_type == 4 and len(binary_data) >= 168:
        # Base station report
        pos_accuracy = extract_bits(binary_data, 78, 1)
        lon_raw = extract_signed_bits(binary_data, 79, 28)
        lat_raw = extract_signed_bits(binary_data, 107, 27)
        
        if lon_raw != 0x6791AC0:
            lon_degrees = lon_raw / 600000.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
                
        if lat_raw != 0x3412140:
            lat_degrees = lat_raw / 600000.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
                
        raim = extract_bits(binary_data, 148, 1)
    
    elif msg_type == 5 and len(binary_data) >= 424:
        # Static & voyage related data
        ais_version = extract_bits(binary_data, 38, 2)
        imo = extract_bits(binary_data, 40, 30)
        callsign = extract_text(binary_data, 70, 7)
        ship_name = extract_text(binary_data, 112, 20)
        ship_type = extract_bits(binary_data, 232, 8)
        dim_a = extract_bits(binary_data, 240, 9)
        dim_b = extract_bits(binary_data, 249, 9)
        dim_c = extract_bits(binary_data, 258, 6)
        dim_d = extract_bits(binary_data, 264, 6)
        pos_accuracy = extract_bits(binary_data, 270, 4)
        
        draught_raw = extract_bits(binary_data, 294, 8)
        if draught_raw is not None and draught_raw > 0:
            draught = f"{draught_raw / 10.0:.1f}"
        
        destination = extract_text(binary_data, 302, 20)
        dte = extract_bits(binary_data, 422, 1)
    
    elif msg_type in [6, 8] and len(binary_data) >= 88:
        # Binary addressed message (6) & Binary broadcast message (8)
        # Only extracting basic fields, payload data not decoded
        pass
    
    elif msg_type == 7 and len(binary_data) >= 72:
        # Binary acknowledge
        # Contains MMSI acknowledgements, basic structure only
        pass
    
    elif msg_type == 9 and len(binary_data) >= 168:
        # SAR aircraft position
        altitude_raw = extract_bits(binary_data, 38, 12)
        if altitude_raw != 4095:
            altitude = altitude_raw
        
        sog_raw = extract_bits(binary_data, 50, 10)
        if sog_raw == 1023:
            sog = "0.0"
        else:
            sog = f"{sog_raw / 10.0:.1f}"
            
        pos_accuracy = extract_bits(binary_data, 60, 1)
        
        lon_raw = extract_signed_bits(binary_data, 61, 28)
        lat_raw = extract_signed_bits(binary_data, 89, 27)
        
        if lon_raw != 0x6791AC0:
            lon_degrees = lon_raw / 600000.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
                
        if lat_raw != 0x3412140:
            lat_degrees = lat_raw / 600000.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
        
        cog_raw = extract_bits(binary_data, 116, 12)
        if cog_raw >= 3600:
            cog = "360.0"
        else:
            cog = f"{cog_raw / 10.0:.1f}"
            
        utc_sec = extract_bits(binary_data, 128, 6)
        dte = extract_bits(binary_data, 142, 1)
        raim = extract_bits(binary_data, 147, 1)
    
    elif msg_type == 10 and len(binary_data) >= 72:
        # UTC & date inquiry
        # Request for UTC, basic structure only
        pass
    
    elif msg_type == 11 and len(binary_data) >= 168:
        # UTC and date response
        pos_accuracy = extract_bits(binary_data, 78, 1)
        lon_raw = extract_signed_bits(binary_data, 79, 28)
        lat_raw = extract_signed_bits(binary_data, 107, 27)
        
        if lon_raw != 0x6791AC0:
            lon_degrees = lon_raw / 600000.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
                
        if lat_raw != 0x3412140:
            lat_degrees = lat_raw / 600000.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
                
        raim = extract_bits(binary_data, 148, 1)
    
    elif msg_type in [12, 14] and len(binary_data) >= 72:
        # Addressed safety related message (12) & Safety related broadcast (14)
        # Text message, basic structure only
        pass
    
    elif msg_type == 13 and len(binary_data) >= 72:
        # Safety related acknowledgement
        # Contains MMSI acknowledgements, basic structure only
        pass
    
    elif msg_type == 15 and len(binary_data) >= 88:
        # Interrogation
        # Request for specific message types, basic structure only
        pass
    
    elif msg_type == 16 and len(binary_data) >= 96:
        # Assignment mode command
        # Station assignment, basic structure only
        pass
    
    elif msg_type == 17 and len(binary_data) >= 80:
        # DGNSS broadcast binary message
        lon_raw = extract_signed_bits(binary_data, 40, 18)
        lat_raw = extract_signed_bits(binary_data, 58, 17)
        
        if lon_raw != 0x1A838:
            lon_degrees = lon_raw / 600.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
                
        if lat_raw != 0xD548:
            lat_degrees = lat_raw / 600.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
    
    elif msg_type == 18 and len(binary_data) >= 168:
        # Class B position report
        sog_raw = extract_bits(binary_data, 46, 10)
        if sog_raw == 1023:
            sog = "0.0"
        else:
            sog = f"{sog_raw / 10.0:.1f}"
            
        pos_accuracy = extract_bits(binary_data, 56, 1)
        
        lon_raw = extract_signed_bits(binary_data, 57, 28)
        lat_raw = extract_signed_bits(binary_data, 85, 27)
        
        if lon_raw != 0x6791AC0:
            lon_degrees = lon_raw / 600000.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
                
        if lat_raw != 0x3412140:
            lat_degrees = lat_raw / 600000.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
        
        cog_raw = extract_bits(binary_data, 112, 12)
        if cog_raw >= 3600:
            cog = "360.0"
        else:
            cog = f"{cog_raw / 10.0:.1f}"
            
        hdg_raw = extract_bits(binary_data, 124, 9)
        if hdg_raw == 511:
            heading = 511
        else:
            heading = hdg_raw
            
        utc_raw = extract_bits(binary_data, 133, 6)
        if utc_raw >= 60:
            utc_sec = 60
        else:
            utc_sec = utc_raw
            
        raim = extract_bits(binary_data, 147, 1)
        sync = extract_bits(binary_data, 149, 2)
        slot = extract_bits(binary_data, 151, 3)
    
    elif msg_type == 19 and len(binary_data) >= 312:
        # Extended Class B
        sog_raw = extract_bits(binary_data, 46, 10)
        if sog_raw == 1023:
            sog = "0.0"
        else:
            sog = f"{sog_raw / 10.0:.1f}"
            
        pos_accuracy = extract_bits(binary_data, 56, 1)
        
        lon_raw = extract_signed_bits(binary_data, 57, 28)
        lat_raw = extract_signed_bits(binary_data, 85, 27)
        
        if lon_raw != 0x6791AC0:
            lon_degrees = lon_raw / 600000.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
                
        if lat_raw != 0x3412140:
            lat_degrees = lat_raw / 600000.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
        
        cog_raw = extract_bits(binary_data, 112, 12)
        if cog_raw >= 3600:
            cog = "360.0"
        else:
            cog = f"{cog_raw / 10.0:.1f}"
            
        hdg_raw = extract_bits(binary_data, 124, 9)
        if hdg_raw == 511:
            heading = 511
        else:
            heading = hdg_raw
            
        utc_raw = extract_bits(binary_data, 133, 6)
        if utc_raw >= 60:
            utc_sec = 60
        else:
            utc_sec = utc_raw
        
        ship_name = extract_text(binary_data, 143, 20)
        ship_type = extract_bits(binary_data, 263, 8)
        dim_a = extract_bits(binary_data, 271, 9)
        dim_b = extract_bits(binary_data, 280, 9)
        dim_c = extract_bits(binary_data, 289, 6)
        dim_d = extract_bits(binary_data, 295, 6)
        
        raim = extract_bits(binary_data, 305, 1)
        dte = extract_bits(binary_data, 306, 1)
    
    elif msg_type == 20 and len(binary_data) >= 72:
        # Data link management message
        # Slot management, basic structure only
        pass
    
    elif msg_type == 21 and len(binary_data) >= 272:
        # Aid to navigation
        aid_type = extract_bits(binary_data, 38, 5)
        ship_name = extract_text(binary_data, 43, 20)
        pos_accuracy = extract_bits(binary_data, 163, 1)
        lon_raw = extract_signed_bits(binary_data, 164, 28)
        lat_raw = extract_signed_bits(binary_data, 192, 27)
        
        if lon_raw != 0x6791AC0:
            lon_degrees = lon_raw / 600000.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
                
        if lat_raw != 0x3412140:
            lat_degrees = lat_raw / 600000.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
        
        dim_a = extract_bits(binary_data, 219, 9)
        dim_b = extract_bits(binary_data, 228, 9)
        dim_c = extract_bits(binary_data, 237, 6)
        dim_d = extract_bits(binary_data, 243, 6)
        
        utc_sec = extract_bits(binary_data, 253, 6)
        off_position = extract_bits(binary_data, 259, 1)
        raim = extract_bits(binary_data, 268, 1)
        
        if len(binary_data) >= 360:
            name_extension = extract_text(binary_data, 272, 14)
    
    elif msg_type == 22 and len(binary_data) >= 168:
        # Channel management
        # Channel assignment, basic structure only
        pass
    
    elif msg_type == 23 and len(binary_data) >= 160:
        # Group assignment command
        # Station assignment, basic structure only
        pass
    
    elif msg_type == 24 and len(binary_data) >= 168:
        # Static data report
        part_num = extract_bits(binary_data, 38, 2)
        
        if part_num == 0:
            # Part A - vessel name
            ship_name = extract_text(binary_data, 40, 20)
        elif part_num == 1:
            # Part B - static data
            ship_type = extract_bits(binary_data, 40, 8)
            callsign = extract_text(binary_data, 90, 7)
            dim_a = extract_bits(binary_data, 132, 9)
            dim_b = extract_bits(binary_data, 141, 9)
            dim_c = extract_bits(binary_data, 150, 6)
            dim_d = extract_bits(binary_data, 156, 6)
    
    elif msg_type == 25 and len(binary_data) >= 40:
        # Single slot binary message
        # Application specific, basic structure only
        pass
    
    elif msg_type == 26 and len(binary_data) >= 60:
        # Multiple slot binary message
        # Application specific, basic structure only
        pass
    
    elif msg_type == 27 and len(binary_data) >= 96:
        # Long range AIS
        pos_accuracy = extract_bits(binary_data, 38, 1)
        raim = extract_bits(binary_data, 39, 1)
        nav_status = extract_bits(binary_data, 40, 4)
        
        lon_raw = extract_signed_bits(binary_data, 44, 18)
        lat_raw = extract_signed_bits(binary_data, 62, 17)
        
        if lon_raw != 0x1A838:
            lon_degrees = lon_raw / 600.0
            longitude, lon_hem = format_lat_lon(lon_degrees, True)
            if longitude is not None:
                longitude = f"{longitude:.7f}"
                
        if lat_raw != 0xD548:
            lat_degrees = lat_raw / 600.0
            latitude, lat_hem = format_lat_lon(lat_degrees, False)
            if latitude is not None:
                latitude = f"{latitude:.7f}"
        
        sog_raw = extract_bits(binary_data, 79, 6)
        if sog_raw == 63:
            sog = "0.0"
        else:
            sog = f"{sog_raw:.1f}"
            
        cog_raw = extract_bits(binary_data, 85, 9)
        if cog_raw >= 360:
            cog = "360.0"
        else:
            cog = f"{cog_raw:.1f}"
        
        gnss = extract_bits(binary_data, 94, 1)
    
    # only return decoded values for valid AIS message types (1-27)
    if msg_type >= 1 and msg_type <= 27:
        return [
            msg_type,
            repeat_ind, 
            mmsi,
            nav_status,
            rot,
            sog,
            pos_accuracy,
            longitude,
            lon_hem,
            latitude,
            lat_hem,
            cog,
            heading,
            utc_sec,
            sync,
            slot,
            raim,
            ship_name,
            ship_type,
            callsign,
            destination,
            draught,
            imo,
            dim_a,
            dim_b,
            dim_c,
            dim_d,
            ais_version,
            dte,
            altitude,
            aid_type,
            name_extension,
            off_position,
            gnss
        ]
    else:
        # invalid message type
        return None

# convert values to CSV format
def make_csv_line(decoded_values):
    if not decoded_values:
        return None
    
    csv_parts = []
    for value in decoded_values:
        if value is None:
            csv_parts.append('0')
        else:
            csv_parts.append(str(value))
    
    return ','.join(csv_parts)

# process the input file
def process_ais_file(input_filename, output_filename=None):
    if output_filename is None:
        output_filename = input_filename.replace('.txt', '_decoded.txt')
    
    try:
        input_file = open(input_filename, 'r')
        output_file = open(output_filename, 'w')
        
        # write header
        header = "message_type,repeat_indicator,mmsi,navigation_status,rate_of_turn,speed_over_ground,position_accuracy,longitude,lon_hemisphere,latitude,lat_hemisphere,course_over_ground,true_heading,utc_second,sync_state,slot_timeout,raim_flag,ship_name,ship_type,callsign,destination,draught,imo,dim_a,dim_b,dim_c,dim_d,ais_version,dte,altitude,aid_type,name_extension,off_position,gnss"
        output_file.write(header + '\n')
        
        total_messages = 0
        decoded_messages = 0
        invalid_messages = 0
        message_types = {}
        invalid_types = {}
        messages_with_position = 0
        valid_without_position = 0
        
        for line in input_file:
            line = line.strip()
            if not line:
                continue
                
            total_messages += 1
            
            # first check if message type is valid by extracting it
            payload = get_payload_from_nmea(line)
            if payload:
                binary_data = convert_payload_to_binary(payload)
                if len(binary_data) >= 6:
                    msg_type_check = extract_bits(binary_data, 0, 6)
                    if msg_type_check is not None and (msg_type_check < 1 or msg_type_check > 27):
                        # invalid message type
                        invalid_messages += 1
                        if msg_type_check in invalid_types:
                            invalid_types[msg_type_check] += 1
                        else:
                            invalid_types[msg_type_check] = 1
                        continue
            
            result = decode_ais(line)
            
            if result:
                msg_type = result[0]
                if msg_type in message_types:
                    message_types[msg_type] += 1
                else:
                    message_types[msg_type] = 1
                
                # check if message has position data
                if result[7] is not None and result[9] is not None:
                    messages_with_position += 1
                else:
                    valid_without_position += 1
                
                csv_line = make_csv_line(result)
                if csv_line:
                    output_file.write(csv_line + '\n')
                    decoded_messages += 1
        
        input_file.close()
        output_file.close()
        
        # print summary
        print(f"Total messages processed: {total_messages}")
        print(f"Successfully decoded: {decoded_messages}")
        print(f"Invalid/non-standard message types: {invalid_messages}")
        print(f"\nValid messages with position data: {messages_with_position}")
        print(f"Valid messages without position data: {valid_without_position}")
        print(f"\nValid message type summary:")
        for msg_type in sorted(message_types.keys()):
            print(f"  Type {msg_type}: {message_types[msg_type]} messages")
        
        if invalid_types:
            print(f"\nInvalid/non-standard message types found:")
            for msg_type in sorted(invalid_types.keys()):
                print(f"  Type {msg_type}: {invalid_types[msg_type]} messages")
        
        print(f"\nDecoded data saved to: {output_filename}")
        
    except FileNotFoundError:
        print(f"Error: Could not find file {input_filename}")
    except Exception as error:
        print(f"Error occurred: {error}")

# debug function to test single message
def debug_single_message(nmea_msg):
    payload = get_payload_from_nmea(nmea_msg)
    if not payload:
        print("Could not parse NMEA message")
        return
        
    binary = convert_payload_to_binary(payload)
    print(f"Payload: {payload}")
    print(f"Binary length: {len(binary)} bits")
    
    msg_type = extract_bits(binary, 0, 6)
    repeat = extract_bits(binary, 6, 2)
    mmsi = extract_bits(binary, 8, 30)
    
    print(f"Message type: {msg_type}")
    print(f"Repeat: {repeat}")
    print(f"MMSI: {mmsi}")
    
    if msg_type in [1, 2, 3]:
        print("Class A position report detected")
        nav_stat = extract_bits(binary, 38, 4)
        rot_raw = extract_signed_bits(binary, 42, 8)
        sog_raw = extract_bits(binary, 50, 10)
        pos_acc = extract_bits(binary, 60, 1)
        lon_raw = extract_signed_bits(binary, 61, 28)
        lat_raw = extract_signed_bits(binary, 89, 27)
        
        print(f"Navigation status: {nav_stat}")
        print(f"ROT raw: {rot_raw}")
        print(f"SOG raw: {sog_raw}")
        print(f"Position accuracy: {pos_acc}")
        print(f"Longitude raw: {lon_raw}")
        print(f"Latitude raw: {lat_raw}")

# main program
if __name__ == "__main__":
    # test with sample message
    test_nmea = "!AIVDM,1,1,,A,38IFDN0Ohj7JvbN0fABtpbJ401w@,0*69"
    print("Testing decoder with sample message:")
    print(f"Input: {test_nmea}")
    
    debug_single_message(test_nmea)
    
    decoded = decode_ais(test_nmea)
    if decoded:
        print(f"Decoded result: {make_csv_line(decoded)}")
    else:
        print("Decoding failed!")
    
    print("\n" + "="*60 + "\n")
    
    # process full file
    input_path = r"C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\nmea-sample"
    output_path = r"C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\nmea-sample_Python_AIS_Decoder_081125"
    
    print(f"Processing AIS messages from: {input_path}")
    process_ais_file(input_path, output_path)
Testing decoder with sample message:
Input: !AIVDM,1,1,,A,38IFDN0Ohj7JvbN0fABtpbJ401w@,0*69
Payload: 38IFDN0Ohj7JvbN0fABtpbJ401w@
Binary length: 168 bits
Message type: 3
Repeat: 0
MMSI: 563451000
Class A position report detected
Navigation status: 0
ROT raw: 127
SOG raw: 50
Position accuracy: 0
Longitude raw: 62256463
Latitude raw: 758091
Decoded result: 3,0,563451000,0,+127.0,5.0,0,103.7607717,E,1.2634850,N,329.8,333,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0

============================================================

Processing AIS messages from: C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\nmea-sample
Total messages processed: 85194
Successfully decoded: 83948
Invalid/non-standard message types: 1232

Valid messages with position data: 77192
Valid messages without position data: 6756

Valid message type summary:
  Type 1: 61418 messages
  Type 2: 2018 messages
  Type 3: 8817 messages
  Type 4: 4074 messages
  Type 5: 2240 messages
  Type 6: 458 messages
  Type 7: 5 messages
  Type 8: 1688 messages
  Type 9: 30 messages
  Type 10: 12 messages
  Type 11: 38 messages
  Type 12: 25 messages
  Type 13: 8 messages
  Type 14: 4 messages
  Type 15: 6 messages
  Type 16: 45 messages
  Type 17: 343 messages
  Type 18: 1387 messages
  Type 19: 219 messages
  Type 20: 323 messages
  Type 21: 404 messages
  Type 22: 29 messages
  Type 23: 6 messages
  Type 24: 338 messages
  Type 25: 2 messages
  Type 26: 4 messages
  Type 27: 7 messages

Invalid/non-standard message types found:
  Type 0: 713 messages
  Type 28: 1 messages
  Type 29: 4 messages
  Type 31: 2 messages
  Type 32: 33 messages
  Type 33: 70 messages
  Type 34: 47 messages
  Type 35: 19 messages
  Type 36: 16 messages
  Type 37: 12 messages
  Type 38: 5 messages
  Type 40: 42 messages
  Type 41: 3 messages
  Type 42: 1 messages
  Type 43: 8 messages
  Type 45: 4 messages
  Type 46: 1 messages
  Type 47: 8 messages
  Type 48: 40 messages
  Type 49: 16 messages
  Type 50: 23 messages
  Type 51: 17 messages
  Type 52: 21 messages
  Type 53: 18 messages
  Type 54: 1 messages
  Type 55: 4 messages
  Type 56: 32 messages
  Type 58: 1 messages
  Type 59: 6 messages
  Type 60: 2 messages
  Type 62: 5 messages
  Type 63: 57 messages

Decoded data saved to: C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\nmea-sample_Python_AIS_Decoder_081125
In [7]:
import pandas as pd
import folium
from IPython.display import display

# Load data
decoded_file = r"C:\Users\cxris\OneDrive\Desktop\VDES research\All_Decoded_L4_Messages\NMEA_Decoded.txt"
df = pd.read_csv(decoded_file)

# convert to numeric and fix hemisphere if needed
df["longitude"] = pd.to_numeric(df["longitude"], errors="coerce")
df["latitude"] = pd.to_numeric(df["latitude"], errors="coerce")
if "lon_hemisphere" in df.columns:
    df.loc[df["lon_hemisphere"].fillna("").str.upper() == "W", "longitude"] *= -1
if "lat_hemisphere" in df.columns:
    df.loc[df["lat_hemisphere"].fillna("").str.upper() == "S", "latitude"] *= -1

# drop invalid coords
df = df.dropna(subset=["longitude", "latitude"])
df = df[(df["longitude"] != 0) & (df["latitude"] != 0)]

# keep last message per MMSI (latest timestamp)
df_plot = df.drop_duplicates(subset=["mmsi"], keep="last").copy()

# center map
center_lat, center_lon = df_plot["latitude"].mean(), df_plot["longitude"].mean()
m = folium.Map(location=[center_lat, center_lon], zoom_start=10)

# add markers with popup 
for _, row in df_plot.iterrows():
    popup_html = (
        f"<b>MMSI:</b> {row['mmsi']}<br>"
        f"<b>SOG:</b> {row.get('speed_over_ground','')} km<br>"
        f"<b>COG:</b> {row.get('course_over_ground','')}°<br>"
        f"<b>Heading:</b> {row.get('true_heading','')}°<br>"
        f"<b>Lat:</b> {row['latitude']:.5f}°<br>"
        f"<b>Lon:</b> {row['longitude']:.5f}°"
    )
    folium.CircleMarker(
        location=[row["latitude"], row["longitude"]],
        radius=5,   
        color="blue",
        fill=True,
        fill_opacity=0.8,
        popup=folium.Popup(popup_html, max_width=300)
    ).add_to(m)

display(m)

# can save as html ltr on if needed:
# m.save("ais_map.html")
Make this Notebook Trusted to load map: File -> Trust Notebook
In [ ]: